from numpy import *
import matplotlib.pyplot as plt
import operator


__author__ = "zjw"


def file2matrix(filename):
    """
    读取文件中的数据，进行格式化处理，并存储到相应的数组中
    :param filename: 要读取的文件路径和文件名
    :return: returnMat, classLabelVector
        returnMat中存储每行3个数据的二维数组，三个数据分别表示：
            1.每年获得的飞行常客历程数
            2.玩视频游戏所耗时间百分比
            3.每周消费的冰淇淋公升数
        classLabelVector中存储的是文件中每行的最后一个字符串
    """

    # 分割字符串，每一个tab键分割出一个字符串
    fr = open(filename)
    # 将文件中的数据以行的形式，存入到arrayOLines
    arrayOLines = fr.readlines()
    # 计算arrayOLines的存储的记录数量
    numberOfLines = len(arrayOLines)
    """ 
        给returnMat这个数组赋值，zeros函数是默认给每个位置赋值为默认值，这里是0
        其中zeros函数中的括号中的内容表示生成一个：
            行数为numberOfLines，列数为3的的二维数组。
    """
    returnMat = zeros((numberOfLines, 3))
    classLabelVector = []
    index = 0
    for line in arrayOLines:
        # 去除一行字符串的首尾空格或换行符
        line = line.strip()
        # 分割字符串，每一个tab键分割出一个字符串
        listFromLine = line.split('\t')
        """
            数组returnMat[index, :]中：
                index是数组的索引值
                冒号":"是切片符
            即将后面的listFromLine[0: 3]数组中取出三个值，加入到returnMat数组的第index行。
        """
        returnMat[index, :] = listFromLine[0: 3]
        # 将文件中的每一行的最后一个字符串依次加入到classLabelVector中。
        classLabelVector.append(int(listFromLine[-1]))
        index += 1
    return returnMat, classLabelVector


def show_file2matrix():
    """
    图形化展示数据，清晰展示三个不同样本分类区域，具有不同爱好的人其类别区域也不同
    :return:
    """

    Mat, Labels = file2matrix('datingTestSet2.txt')
    # figure()操作时创建或者调用画板，使用时遵循就近原则，所有画图操作是在最近一次调用的画图板上实现。
    fig = plt.figure('特征关系图')
    # add_subplot()函数表示在图像在网格中显示的位置。111表示“1×1网格数，第1个网格，(111)可以替代为(1, 1, 1)
    ax = fig.add_subplot(111)
    """
        scatter()函数为绘图功能，其中：
            第一个参数和第二个参数分别表示x轴和y轴的坐标，一一对应。
            第三个参数表示图中显示的点的大小，这里×15是为了让点更大，更清晰。
            第四个参数表示图中点的颜色，使用Labels数组中的不同值使不同类型显示不同颜色。
            这里的x轴和y轴的值选择的是数据集中的第1列和第2列主要原因：
                三种类型在这两种特征下区分比较明显。
    """
    ax.scatter(Mat[:, 0], Mat[:, 1], 15 * array(Labels), 15.0 * array(Labels))
    plt.show()  # 显示图像


def autoNorm(dataSet):
    """
    数值归一化，可以自动将数字特征值转化为0到1区间的值，表示所占比例
    归一化处理的目的是：让数据中所有特征的权重一样。
    数值归一化公式：（特征值-min）/（max-min）
    :param dataSet: 传入上面已经格式化好的数据数组
    :return:normDataSet, ranges, minVals
        normDataSet：数值归一化后的数组
        ranges：1×3数组，maxVals - minVals
        minVals：数组中每列选取的最小值
    """

    """
        dataSet.min(0)中的参数0使得函数可以从每列中选取最小值，而不是选取当前行的最小汉字。
        同理：dataSet.max(0)是从数组的每列中选取最大值
        这里获得的minVals和maxVals都是1×3的数组
    """
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    # 计算（max-min）的部分
    ranges = maxVals - minVals
    # 生成一个和dataSet同行数同列数的数组，数组中的数据全部填充为0
    normDataSet = zeros(shape(dataSet))
    """
        shape方法本身返回的是数组的结构，例：二维数组：返回的是(m, n)，表示m行n列
        此时shape[0]中就是m行的意思了
    """
    m = dataSet.shape[0]
    """
        tile函数是对数据进行格式化处理
        tile(minVals, (m, 1))中的minVals表示要被格式化的数组，
            (m, 1)表示生成一个m行，每行一个minVals数组的数组
    """
    normDataSet = dataSet - tile(minVals, (m, 1))  # 就算（特征值-min）部分
    # 数值归一化公式：（特征值-min）/（max-min）
    normDataSet = normDataSet / tile(ranges, (m, 1))
    return normDataSet, ranges, minVals


def classify(inX, dataSet, labels, k):
    """
    分类器，训练模块，计算某些特征所属类型
    :param inX: 传入需要测试的列表
    :param dataSet: 特征集合
    :param labels: 类别集合
    :param k: 匹配次数
    :return: 返回训练结果，即所属类型
    """

    # 因为dataSet是一个二维数组，(m, n)表示m行n列，shape[0]获得m行的数量
    dataSetSize = dataSet.shape[0]
    """
        tile函数用于生成一个dataSetSize行数，inX列数的二维数组
        将新生成的二维数组的每个值依次减去dataSet这个数组中的各个值
    """
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet
    # diffMat数组中的各个值平方
    sqDiffMat = diffMat ** 2
    # .sum(axis=1)中axis为1时表示按行的方向相加；axis为0时表示按列的方向相加
    sqDistances = sqDiffMat.sum(axis=1)
    # 再将上面得到的各个和值分别开根号，求得两点之间的距离
    distances = sqDistances ** 0.5
    """
        argsort()函数功能说明：返回的是数组值从小到大的索引值。
            例：[3, 2, 1]这个数组，使用argsort()函数，经历的步骤：
            1.排序：[1, 2, 3]
            2.得到排序后各个值所对应的原数组中的索引：1对应的索引值为2,2对应的索引值为1,3对应的索引值为0,
            3.最后返回的数组为[2, 1, 0]
    """
    # argsort函数返回的是distances这个数组中的值从小到大的索引值
    sortedDistIndicies = distances.argsort()
    classCount = {}
    # 该for语句用来取出离目标数据最近的k个点
    for i in range(k):
        # 通过获得的最小k个值在原来数组中的索引值，可以从labels数组中得到该索引下所对应的喜欢类型和程度
        voteIlabel = labels[sortedDistIndicies[i]]
        # 字典处理方式，get方法中voteIlabel是key值，0是当字典中不存在voteIlabel时，给它赋默认值0
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
    # 对classCount这个字典的value值进行逆序排序，使得取第一个数为最大值
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    # 返回字典中的最大值，即算法计算得到的最接近的类型
    return sortedClassCount[0][0]


def datingClassTest():
    """
    选取10%的数据来测试分类器的性能，即测试分类器分析数据的错误率。
    :return:
    """
    hoRatio = 0.10
    datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
    normMat, ranges, minVals = autoNorm(datingDataMat)
    # 因为normMat是一个二维数组，(m, n)表示m行n列，shape[0]获得m行的数量
    m = normMat.shape[0]
    # 取总数据的10%作为测试数据
    numTestVecs = int(m * hoRatio)
    errorCount = 0.0
    """
        遍历所有测试数据，让测试数据原文件中正确的结果和分类器得到的结果进行对比，
        对不正确的情况的数量进行统计，用于最后计算错误率。
    """
    for i in range(numTestVecs):
        """
            切片理解：
                normMat[i, :]，其中方括号中的逗号前后分别表示对行和列的切片，:前后没有值，表示取所有列的值，整个表示取第i行所有列的值
                normMat[numTestVecs: m, :]，同上，整个表示取出numTestVecs行到m行的所有列的值
                datingLabels[numTestVecs: m]，表示取出numTestVecs列到m列的值
        """
        classifierResult = classify(normMat[i, :], normMat[numTestVecs: m, :],
                                    datingLabels[numTestVecs: m], 3)
        print("%d: 分类器返回的结果是： %d, 真正的答案是：%d" % (i,
                                                classifierResult, datingLabels[i]))
        # 当分类器得到的类型和原数据中的类型不同时，错误数加1
        if classifierResult != datingLabels[i]:
            errorCount += 1.0
    print("分类器处理约会数据集的错误率为: %f" % (errorCount / float(numTestVecs)))


def classifyPerson():
    """
    通过数据处理和分类器的筛选，得到相应特征下的人所属的类别。
    :return:
    """
    resultList = ['不喜欢的人', '魅力一般的人', '极具魅力的人']
    ffMiles = float(input("每年获得的飞行常客历程数："))
    percentTats = float(input("玩视频游戏所耗时间百分比："))
    iceCream = float(input("每年消费冰淇淋公升数："))
    datingDataMat, datingLabels = file2matrix("datingTestSet2.txt")
    normMat, ranges, minVals = autoNorm(datingDataMat)
    # 将上面用户输入的数据，整合到一个数组中
    inArr = array([ffMiles, percentTats, iceCream])
    # (inArr - minVals) / ranges 对inArr中的数值进行归一化处理
    classifierResult = classify((inArr - minVals) / ranges, normMat, datingLabels, 3)
    print("你对这个人的印象是：", resultList[classifierResult - 1])


if __name__ == '__main__':
    show_file2matrix()  # 用图像展示特征值对于最终类型的影响
    datingClassTest()  # 分类器处理约会数据集的错误率为: 0.050000
    classifyPerson()
